package com.hk.commons;

import java.nio.ByteBuffer;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Antinomy
 */
public class LongIdThreadSafety {

    private static final long EPOCH = 1388505600000L;
    private static final long MAX_SIGNED_LONG = 2199023255551L;

    private static final int TIME_SHIFT = 10; //22
    private static final int RANDOM_SHIFT = 42;
    public static final int MAX_TIME_MILLIS_CHANGE = 1000 * 10;

    private AtomicLong lastTimestamp = new AtomicLong(0);

    private Set<Long> recentRandoms = ConcurrentHashMap.<Long>newKeySet();

    static LongIdThreadSafety inst = new LongIdThreadSafety();

    public static long get() {
        return inst.generate();
    }

    /**
     * Generates a Twitter Snowflake compatible id utilizing randomness for the right most 22 bits and
     * the Twitter standard EPOCH
     *
     * @return long
     */
    synchronized public long generate() {

        long currentTimestamp = System.currentTimeMillis();

        while (lastTimestamp.get() > currentTimestamp) {
            // Clock is running backwards so wait until it isn't
            currentTimestamp = System.currentTimeMillis();
        }

        if (lastTimestamp.get() != currentTimestamp) {
            // timestamp has advanced so reset it and clear the previous cache
            lastTimestamp.lazySet(currentTimestamp);
        }

        if (lastTimestamp.get() - currentTimestamp > MAX_TIME_MILLIS_CHANGE) {
            recentRandoms.clear();
        }

        if (currentTimestamp < EPOCH || currentTimestamp > MAX_SIGNED_LONG) {
            // The current time cannot be less than the EPOCH
            throw new RuntimeException("Invalid System Clock was " + new Date(currentTimestamp));
        }

        final long customTimestamp = currentTimestamp - EPOCH;

        final long shiftedTimestamp = customTimestamp << TIME_SHIFT;

        AtomicReference<Long> result = new AtomicReference<>(null);
        AtomicReference<Long> random = new AtomicReference<>(nextRandomPart());

        makeUniqueId(shiftedTimestamp, result, random);

//        System.out.println(result.get() + " is build by " + shiftedTimestamp + " and " + random.get()
//                + " on time " + new Date(currentTimestamp));

        return result.get();
    }

    private void makeUniqueId(long shiftedTimestamp, AtomicReference<Long> result, AtomicReference<Long> random) {
        result.set(shiftedTimestamp | random.get());

        while (recentRandoms.contains(result.get())) {
            random.set(nextRandomPart());
            result.set(shiftedTimestamp | random.get());
        }

        if (!recentRandoms.contains(result.get())) {
            recentRandoms.add(result.get());
        }
    }

    /**
     * Generates a Twitter Snowflake compatible id utilizing randomness for the right most 22 bits and
     * the Twitter standard EPOCH
     *
     * @return byte[]
     */
    public byte[] generateBytes() {
        return ByteBuffer.allocate(8).putLong(generate()).array();
    }

    private long nextRandomPart() {
        return ThreadLocalRandom.current().nextLong() >>> RANDOM_SHIFT;
    }
}